package ru.ifmo.sound.neuro;

import net.sf.javaml.core.Instance;
import ru.ifmo.sound.jmlbased.WrapperMethods;

import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * @author avhaliullin
 */
public class NeuroNet {
    private List<List<Neuron>> layers = new ArrayList<List<Neuron>>();
    private double educationSpeed = 0.1;

    public NeuroNet(int n, double educationSpeed, LayerBuilder... builders) {
        this.educationSpeed = educationSpeed;
        List<Neuron> first = new ArrayList<Neuron>();
        for (int i = 0; i < n; i++) {
            first.add(new Neuron());
        }
        layers.add(first);
        for (LayerBuilder builder : builders) {
            layers.add(builder.build(layers.get(layers.size() - 1)));
        }
        List<Neuron> last = layers.get(layers.size() - 1);
        layers.add(NeuronetFactory.createMergingLayer(1).build(last));
    }

    private double f(double a) {
        return 1 / (1 + Math.exp(-a));
    }

    private double derivativeF(double a) {
        double sx = f(a);
        return sx * (1 - sx);
    }

    private double count(Neuron neuron) {

        neuron.e = 0d;
        if (neuron.y != null) {
            return neuron.y;
        }

        for (Neuron.WeightedNeuron weightedNeuron : neuron.prevLayer) {
            neuron.e += weightedNeuron.weight * count(weightedNeuron.neuron);
        }
        neuron.y = f(neuron.e);
        return neuron.y;
    }

    private double check(double[] input) {
        for (List<Neuron> layer : layers) {
            for (Neuron neuron : layer) {
                neuron.reset();
            }
        }
        int i = 0;
        for (Neuron neuron : layers.get(0)) {
            neuron.y = input[i++];
        }
        return count(layers.get(layers.size() - 1).get(0));
    }

    public boolean query(Instance instance) {
        return query(WrapperMethods.instanceToArray(instance));
    }

    public boolean learn(Instance instance, boolean correctAnswer) {
        return learn(WrapperMethods.instanceToArray(instance), correctAnswer);
    }

    public boolean query(double[] input) {
//        return learn(input, false);
        double res = check(input);
        System.out.println(String.format("%.2f", res));
        return res > 1d / 2;
    }

    public boolean learn(double[] input, boolean correctAnswer) {
        double res = check(input);
        double error = (correctAnswer ? 1 : 0) - res;

        layers.get(layers.size() - 1).get(0).d = error;
        for (int i = layers.size() - 1; i > 0; i--) {
            for (Neuron neuron : layers.get(i)) {
                for (Neuron.WeightedNeuron weightedNeuron : neuron.prevLayer) {
                    weightedNeuron.neuron.d += weightedNeuron.weight * neuron.d;
                    if (neuron.e == null) {
                        System.out.println(i);
                    }
                    weightedNeuron.weight += educationSpeed * neuron.d * derivativeF(neuron.e) * weightedNeuron.neuron.y;
                }
            }
        }

        return res > 1d / 2;
    }
}
